Skip to content

perf(moe): two-kernel decode-MoE that beats gather_qmm (#268 step 2b) - #276

Merged
inureyes merged 1 commit into
mainfrom
perf/issue-268-fused-moe-2b
Jun 14, 2026
Merged

perf(moe): two-kernel decode-MoE that beats gather_qmm (#268 step 2b)#276
inureyes merged 1 commit into
mainfrom
perf/issue-268-fused-moe-2b

Conversation

@inureyes

Copy link
Copy Markdown
Member

Summary

Step 2b of the fused decode-MoE kernel effort (#268). The 2a kernel (#275) was correctness-validated but ran one threadgroup per token (one GPU core), so it was ~88x slower than gather_qmm. This PR parallelizes it into a kernel that beats gather_qmm by ~3.5% on qwen3-30b-a3b, greedy output byte-identical.

What changed

Two steps, each benched on qwen3-30b-a3b (M1 Ultra), greedy byte-identical throughout:

  1. SIMD-cooperative GEMV (one simdgroup per output row, 32 lanes stride the contraction dim + simd_sum): 0.54 → 21.6 tok/s (40x over 2a), but still 0.46x of gather_qmm. An occupancy sweep (redundant tiling to force K·R threadgroups) confirmed the kernel is occupancy-bound, not tuning-bound — and that a fully-fused single launch can't escape it: the gate/up → down dependency runs through the per-expert activation, pinning each expert to one threadgroup.

    threadgroups 8 (K) 16 32 64
    decode tok/s 21.6 24.9 26.4 27.4
  2. Two-kernel non-redundant split: stage the swiglu activation in global memory across two dispatches (A: gate/up + swiglu → act_g[K, Dff]; B: down · score → partial[K, Din], summed over K). Every GEMV output row is now an independent simdgroup across all cores, each weight read exactly once: 49.0 vs 47.3 tok/s, +3.5%.

Validation

  • Greedy temp-0 output byte-identical to the gather_qmm path on qwen3-30b-a3b (3 prompts) and qwen1.5-moe-a2.7b.
  • qwen1.5-moe-a2.7b (~140 tok/s) is at parity (no regression); the small/fast MoE has less to gain.
  • cargo fmt --check clean, cargo clippy --release --features metal,accelerate clean.

Scope / flags

Single-token decode, 4/8-bit affine only, off by default (MLXCEL_FUSED_MOE). MLXCEL_FUSED_MOE_SGY tunes simdgroups per threadgroup (default 8). The single-launch kernel and its diagnostic SG/R knobs are removed in favor of the two-kernel implementation.

Next (not in this PR)

  • Step 2c: 6-bit / mixed-bit unpack so dots.llm1 (6-bit down_proj, the 0.51x target) and nemotron-h MoE benefit instead of falling back.
  • Broader-fleet validation and a decision on flipping MLXCEL_FUSED_MOE on by default.

Refs #268.

Parallelize the fused decode-MoE expert kernel from step 2a so it actually
beats the gather_qmm baseline instead of underutilizing the GPU.

The 2a kernel was correct but ran one threadgroup per token (one GPU core),
making it ~88x slower than gather_qmm. Two changes fix that, validated greedy
byte-identical throughout on qwen3-30b-a3b (M1 Ultra):

1. SIMD-cooperative GEMV (one simdgroup per output row, 32 lanes stride the
   contraction dim + simd_sum) lifts it to 21.6 tok/s (40x over 2a), but still
   0.46x of gather_qmm. An occupancy sweep confirmed the kernel is
   occupancy-bound: a single fused launch pins each expert to one threadgroup
   because the gate/up -> down dependency runs through the on-chip activation,
   so it cannot fill more than K cores without recomputing gate/up redundantly.

2. Break that barrier: stage the swiglu activation in global memory across two
   dispatches (A: gate/up + swiglu -> act_g[K, Dff]; B: down * score ->
   partial[K, Din], summed over K). Every GEMV output row is now an independent
   simdgroup across all GPU cores, each weight read exactly once. Result: 49.0
   vs 47.3 tok/s, +3.5% over gather_qmm. qwen1.5-moe-a2.7b is at parity (no
   regression); the small/fast MoE has less to gain.

Still single-token decode, 4/8-bit affine only, off by default
(MLXCEL_FUSED_MOE); MLXCEL_FUSED_MOE_SGY tunes simdgroups per threadgroup
(default 8). The single-launch kernel and its diagnostic SG/R knobs are
removed in favor of the two-kernel implementation. 6-bit (dots.llm1) is the
next step.
@inureyes inureyes added type:performance Performance improvements area:models Model architectures, weights, loading, metadata area:inference Generation, sampling, decoding (incl. speculative, DRY) platform:macos macOS (Apple Silicon) specific priority:medium Medium priority labels Jun 14, 2026
@inureyes
inureyes merged commit 65abbd5 into main Jun 14, 2026
5 checks passed
@inureyes
inureyes deleted the perf/issue-268-fused-moe-2b branch June 14, 2026 03:19
@inureyes inureyes added this to the 0.3 milestone Jun 21, 2026
@inureyes inureyes self-assigned this Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:inference Generation, sampling, decoding (incl. speculative, DRY) area:models Model architectures, weights, loading, metadata platform:macos macOS (Apple Silicon) specific priority:medium Medium priority type:performance Performance improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant